使用内核驱动上手 x710 flow directory 功能
环境介绍
i40e 驱动版本为 5.0 内核主线版本,网卡为 x710 网卡。需要测试 flow directory 功能能否正常使用。
第一个问题:不支持 flow-type 为 ether
使用下面这条命令,设定一个 flow-type 为 ether 类型的过滤条件时报了 NOTSUPP 的错误。
ethtool -U enp11s0f0 flow-type ether ....
分析 flow-type 为 ether 时的执行过程
ethtool 中的流程分析
ethtool 中解析代码如下:
else if (!strcmp(argp[0], "ether"))
flow_type = ETHER_FLOW;
fsp->flow_type = flow_type;
当 ethtool 命令行中设定了 flow-type 为 ether 时,ethtool 会将 flow_type 设定为 ETHER_FLOW。
然后立刻判断 flow_type,分发到不同的 options 中,相关代码如下:
case ETHER_FLOW:
options = rule_nfc_ether;
n_opts = ARRAY_SIZE(rule_nfc_ether);
break;
options 表示解析参数的规则,rule_nfc_ether 定义如下:
static const struct rule_opts rule_nfc_ether[] = {
{ "src", OPT_MAC, NFC_FLAG_SADDR,
offsetof(struct ethtool_rx_flow_spec, h_u.ether_spec.h_source),
offsetof(struct ethtool_rx_flow_spec, m_u.ether_spec.h_source) },
{ "dst", OPT_MAC, NFC_FLAG_DADDR,
offsetof(struct ethtool_rx_flow_spec, h_u.ether_spec.h_dest),
offsetof(struct ethtool_rx_flow_spec, m_u.ether_spec.h_dest) },
{ "proto", OPT_BE16, NFC_FLAG_PROTO,
offsetof(struct ethtool_rx_flow_spec, h_u.ether_spec.h_proto),
offsetof(struct ethtool_rx_flow_spec, m_u.ether_spec.h_proto) },
{ "action", OPT_U64, NFC_FLAG_RING,
offsetof(struct ethtool_rx_flow_spec, ring_cookie), -1 },
{ "vf", OPT_RING_VF, NFC_FLAG_RING_VF,
offsetof(struct ethtool_rx_flow_spec, ring_cookie), -1 },
{ "queue", OPT_RING_QUEUE, NFC_FLAG_RING_QUEUE,
offsetof(struct ethtool_rx_flow_spec, ring_cookie), -1 },
{ "loc", OPT_U32, NFC_FLAG_LOC,
offsetof(struct ethtool_rx_flow_spec, location), -1 },
{ "vlan-etype", OPT_BE16, NTUPLE_FLAG_VETH,
offsetof(struct ethtool_rx_flow_spec, h_ext.vlan_etype),
offsetof(struct ethtool_rx_flow_spec, m_ext.vlan_etype) },
{ "vlan", OPT_BE16, NTUPLE_FLAG_VLAN,
offsetof(struct ethtool_rx_flow_spec, h_ext.vlan_tci),
offsetof(struct ethtool_rx_flow_spec, m_ext.vlan_tci) },
{ "user-def", OPT_BE64, NTUPLE_FLAG_UDEF,
offsetof(struct ethtool_rx_flow_spec, h_ext.data),
offsetof(struct ethtool_rx_flow_spec, m_ext.data) },
};
解析完所有的参数同时配置好 struct ethtool_rx_flow_spec 结构体,然后调用 ioctl 来下发配置到网卡。
配置方式有两种类型,相关代码如下:
/* attempt to add rule via N-tuple specifier */
err = do_srxntuple(ctx, &rx_rule_fs);
if (!err)
return 0;
/* attempt to add rule via network flow classifier */
err = rxclass_rule_ins(ctx, &rx_rule_fs, rss_context);
if (err < 0) {
fprintf(stderr, "Cannot insert"
" classification rule\n");
return 1;
}
第一种配置方式是 ntuple,第二种是 classifier,这两种不同方式传递给 ioctl 的 ethtool 子命令存在区别。
当驱动不支持 ntuple 方式配置时(返回值为 EOPNOTSUPP 时),不会打印失败信息,继续尝试 classifier 方式。
当两种方式都失败后,软件执行如下代码打印错误信息:
fprintf(stderr, "Cannot insert"
" classification rule\n");
ntuple 方式 ioctl 的参数
/* send rule via N-tuple */
ntuplecmd.cmd = ETHTOOL_SRXNTUPLE;
err = send_ioctl(ctx, &ntuplecmd);
classifier 方式 ioctl 的参数
/* notify netdev of new rule */
nfccmd.cmd = ETHTOOL_SRXCLSRLINS;
nfccmd.rss_context = rss_context;
nfccmd.fs = *fsp;
err = send_ioctl(ctx, &nfccmd);
i40e 内核驱动支持哪一种类型?
5.0 内核中,ethtool 框架不支持 ntuple 方式。
ioctl 的内核流程
ethool 调用 ioctl 最终会走到 dev_ethtool 函数中,在这个函数中 ETHTOOL_SRXCLSRLINS 被分发到 ethtool_set_rxnfc 函数中,相关代码如下:
2705 case ETHTOOL_SRXFH:
2706 case ETHTOOL_SRXCLSRLDEL:
2707 case ETHTOOL_SRXCLSRLINS:
2708 rc = ethtool_set_rxnfc(dev, ethcmd, useraddr);
2709 break;
ethtool_set_rxnfc 核心代码如下:
rc = dev->ethtool_ops->set_rxnfc(dev, &info);
可以看到它调用了 ethtool_ops 中的 set_rxnfc 函数指针,此函数指针在 i40e_ethtool.c 中实现,i40e_ethtool.c 中实现的函数代码如下:
4308 static int i40e_set_rxnfc(struct net_device *netdev, struct ethtool_rxnfc *cmd)
4309 {
4310 struct i40e_netdev_priv *np = netdev_priv(netdev);
4311 struct i40e_vsi *vsi = np->vsi;
4312 struct i40e_pf *pf = vsi->back;
4313 int ret = -EOPNOTSUPP;
4314
4315 switch (cmd->cmd) {
4316 case ETHTOOL_SRXFH:
4317 ret = i40e_set_rss_hash_opt(pf, cmd);
4318 break;
4319 case ETHTOOL_SRXCLSRLINS:
4320 ret = i40e_add_fdir_ethtool(vsi, cmd);
4321 break;
4322 case ETHTOOL_SRXCLSRLDEL:
4323 ret = i40e_del_fdir_entry(vsi, cmd);
4324 break;
4325 default:
4326 break;
4327 }
4328
4329 return ret;
4330 }
i40e_add_fdir_ethtool 在真正设定到网卡前做了许多检查,其中有 i40e_check_fdir_input_set 这个对 input_set 的检查。
i40e_check_fdir_input_set 中判断 flow_type,当这个字段设定为 ETHER_FLOW 此函数将会返回 -EOPNOTSUPP。相关代码如下:
3801 switch (fsp->flow_type & ~FLOW_EXT) {
3802 case SCTP_V4_FLOW:
3803 index = I40E_FILTER_PCTYPE_NONF_IPV4_SCTP;
3804 fdir_filter_count = &pf->fd_sctp4_filter_cnt;
3805 break;
3806 case TCP_V4_FLOW:
3807 index = I40E_FILTER_PCTYPE_NONF_IPV4_TCP;
3808 fdir_filter_count = &pf->fd_tcp4_filter_cnt;
3809 break;
3810 case UDP_V4_FLOW:
3811 index = I40E_FILTER_PCTYPE_NONF_IPV4_UDP;
3812 fdir_filter_count = &pf->fd_udp4_filter_cnt;
3813 break;
3814 case IP_USER_FLOW:
3815 index = I40E_FILTER_PCTYPE_NONF_IPV4_OTHER;
3816 fdir_filter_count = &pf->fd_ip4_filter_cnt;
3817 flex_l3 = true;
3818 break;
3819 default:
3820 return -EOPNOTSUPP;
3821 }
可以确定对于执行 ethtool -U devname ether xxx 的设定 linux 5.0 的 i40e 驱动并不支持!
第二个问题:如何丢掉所有的 ipv4 udp 报文?
在测试前我首先执行 ifconfig enp11s0f0 -promisc 命令打开混淆模式。
构造 udp 报文开始打流,然后执行如下命令能够丢掉所有的 ipv4 udp 报文:
ethtool -U enp11s0f0 flow-type udp4 action -1
设定完成可以通过 ethtool -u enp11s0f0 来查看配置内容,同时可以通过执行如下命令查看 fdir_match 计数确定配置生效:
ethtool -S enp11s0f0 | grep fdir
设定后会得到一个 id,我测试时 id 是 7679,可以使用 ethtool -U enp11s0f0 delete 7679 来移除这一条配置。
第三个问题:如何使用 flex filter 功能?
linux 内核源码树 networking/device_drivers/intel/i40e.rst 文件中描述了可以通过使用 user-def 字段来匹配协议的 payload 中的字段内容。
manual 说明贴到下面:
The driver also supports matching user-defined data within the packet payload.
This flexible data is specified using the "user-def" field of the ethtool
command in the following way:
+----------------------------+--------------------------+
| 31 28 24 20 16 | 15 12 8 4 0 |
+----------------------------+--------------------------+
| offset into packet payload | 2 bytes of flexible data |
+----------------------------+--------------------------+
For example,
::
... user-def 0x4FFFF ...
tells the filter to look 4 bytes into the payload and match that value against
0xFFFF. The offset is based on the beginning of the payload, and not the
beginning of the packet. Thus
::
flow-type tcp4 ... user-def 0x8BEAF ...
would match TCP/IPv4 packets which have the value 0xBEAF 8 bytes into the
TCP/IPv4 payload.
上述说明信息讲了下面这几个内容:
- user-def 只能从 payload 开始匹配
- user-def 由 4 字节组成,高 16 位的两个字节代表相对 payload 的偏移量,低 16 位为匹配的内容
由于 flexible data 的单位是两个字节,offset 只支持偶数,不能使用奇数。实际测试时,我发现按照上面的描述配置能够正常配置,但是过滤功能并不生效。
唯一一次生效的命令如下:
ethtool -U enp11s0f0 flow-type udp4 user-def 0x0 action -1
Added rule with ID 7679
测试仪发送的 udp 报文,其 payload 的前两个字节都是 0,dmesg 有如下信息:
[165763.336866] i40e 0000:0b:00.0 enp11s0f0: Input set change requested for udp4 flows:
[165763.336871] i40e 0000:0b:00.0 enp11s0f0: Current input set: 2000
[165763.336874] i40e 0000:0b:00.0 enp11s0f0: Requested input set: 2000
[165763.336877] i40e 0000:0b:00.0 enp11s0f0: FLEX index 0: Offset -> 0
根据上面的描述,当针对 UDPv4 设置 flex 的时候,payload 的起始位置指向的是 UDPv4 报文的 data 区域起始位置。
我构造如下 udp payload 内容进行测试:
测试命令如下:
ethtool -U enp11s0f0 flow-type udp4 user-def 0x9013 action -1
dmesg 的打印信息如下:
[163317.273795] i40e 0000:0b:00.0 enp11s0f0: Input set change requested for udp4 flows:
[163317.273801] i40e 0000:0b:00.0 enp11s0f0: Current input set: 2000
[163317.273805] i40e 0000:0b:00.0 enp11s0f0: Requested input set: 2000
[163317.273808] i40e 0000:0b:00.0 enp11s0f0: FLEX index 0: Offset -> 0
可以确定设置成功了,但是没有生效,怀疑大小端、偏移量的影响并都进行了排查,没有找到问题。
对 flex filter 的认识
flex filter 是针对报文的 payload 进行 filter,也就是说我们无法针对报文的 header 进行过滤,但是在网络报文封装中,上层的协议帧可以看做是下层协议帧的 payload.
也就是说如果我们要过滤一个 96 字节的 udp 报文,那么要设定过滤一个 ip 报文的命令,因为在过滤 ip 报文的时候 udp 的头是可见的,有了这个知识,然后计算出 96 字节支持的 udp 包的大小,编写命令。
同时需要了解的是接口的混淆模式可能对过滤条件有影响,根据测试情况看,在开混淆模式后过滤功能仍然能够生效。
有了上面的理解,我继续尝试过滤 udp 报文的 payload 字段,结果都失败了,网上搜索了下发现相关的内容非常少,看来只有怼代码了!
尝试分析代码以找到 flex filter 不能生效的原因
研究代码,发现设定一个 flow directory 规则的流程大致如下:
- 选择 input set
- 获取 fdir 对应的 ring
- 根据 flow-type 等参数设定一个 i40e_filter_program_desc 描述符内容
- 构造一个报文,此报文根据 flow-type 等参数来设定,它也需要占一个描述符
- 写入 tail 让网卡处理这两个描述符
- 检查设定是否生效
i40e_fdir 函数负责填充 i40e_filter_program_desc 描述符,这个描述符的定义如下:
struct i40e_filter_program_desc {
__le32 qindex_flex_ptype_vsi;
__le32 rsvd;
__le32 dtype_cmd_cntindex;
__le32 fd_id;
};
i40e_fdir 代码如下:
static void i40e_fdir(struct i40e_ring *tx_ring,
struct i40e_fdir_filter *fdata, bool add)
{
struct i40e_filter_program_desc *fdir_desc;
struct i40e_pf *pf = tx_ring->vsi->back;
u32 flex_ptype, dtype_cmd;
u16 i;
/* grab the next descriptor */
i = tx_ring->next_to_use;
fdir_desc = I40E_TX_FDIRDESC(tx_ring, i);
i++;
tx_ring->next_to_use = (i < tx_ring->count) ? i : 0;
flex_ptype = I40E_TXD_FLTR_QW0_QINDEX_MASK &
(fdata->q_index << I40E_TXD_FLTR_QW0_QINDEX_SHIFT);
flex_ptype |= I40E_TXD_FLTR_QW0_PCTYPE_MASK &
(fdata->pctype << I40E_TXD_FLTR_QW0_PCTYPE_SHIFT);
flex_ptype |= I40E_TXD_FLTR_QW0_PCTYPE_MASK &
(fdata->flex_offset << I40E_TXD_FLTR_QW0_FLEXOFF_SHIFT);
/* Use LAN VSI Id if not programmed by user */
flex_ptype |= I40E_TXD_FLTR_QW0_DEST_VSI_MASK &
((u32)(fdata->dest_vsi ? : pf->vsi[pf->lan_vsi]->id) <<
I40E_TXD_FLTR_QW0_DEST_VSI_SHIFT);
dtype_cmd = I40E_TX_DESC_DTYPE_FILTER_PROG;
dtype_cmd |= add ?
I40E_FILTER_PROGRAM_DESC_PCMD_ADD_UPDATE <<
I40E_TXD_FLTR_QW1_PCMD_SHIFT :
I40E_FILTER_PROGRAM_DESC_PCMD_REMOVE <<
I40E_TXD_FLTR_QW1_PCMD_SHIFT;
dtype_cmd |= I40E_TXD_FLTR_QW1_DEST_MASK &
(fdata->dest_ctl << I40E_TXD_FLTR_QW1_DEST_SHIFT);
dtype_cmd |= I40E_TXD_FLTR_QW1_FD_STATUS_MASK &
(fdata->fd_status << I40E_TXD_FLTR_QW1_FD_STATUS_SHIFT);
if (fdata->cnt_index) {
dtype_cmd |= I40E_TXD_FLTR_QW1_CNT_ENA_MASK;
dtype_cmd |= I40E_TXD_FLTR_QW1_CNTINDEX_MASK &
((u32)fdata->cnt_index <<
I40E_TXD_FLTR_QW1_CNTINDEX_SHIFT);
}
fdir_desc->qindex_flex_ptype_vsi = cpu_to_le32(flex_ptype);
fdir_desc->rsvd = cpu_to_le32(0);
fdir_desc->dtype_cmd_cntindex = cpu_to_le32(dtype_cmd);
fdir_desc->fd_id = cpu_to_le32(fdata->fd_id);
}
上面的代码中,dtype_cmd 是个非常重要的字段,它经过一系列的判断设定其值。
i40e_fdir 中设定描述符字段内容完成后,将 tx_ring->next_to_use 向后拨一个单位,表示占用了一个描述符。
i40e_fdir 填充完成后,继续填充 raw_packet,占据下一个描述符,并设定相关的字段。
过滤 udp 报文时构造 raw_packet 报文的代码如下:
static char packet[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x08, 0,
0x45, 0, 0, 0x1c, 0, 0, 0x40, 0, 0x40, 0x11, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
raw_packet = kzalloc(I40E_FDIR_MAX_RAW_PACKET_SIZE, GFP_KERNEL);
if (!raw_packet)
return -ENOMEM;
memcpy(raw_packet, packet, I40E_UDPIP_DUMMY_PACKET_LEN);
ip = (struct iphdr *)(raw_packet + IP_HEADER_OFFSET);
udp = (struct udphdr *)(raw_packet + IP_HEADER_OFFSET
+ sizeof(struct iphdr));
ip->daddr = fd_data->dst_ip;
udp->dest = fd_data->dst_port;
ip->saddr = fd_data->src_ip;
udp->source = fd_data->src_port;
if (fd_data->flex_filter) {
u8 *payload = raw_packet + I40E_UDPIP_DUMMY_PACKET_LEN;
__be16 pattern = fd_data->flex_word;
u16 off = fd_data->flex_offset;
*((__force __be16 *)(payload + off)) = pattern;
}
fd_data->pctype = I40E_FILTER_PCTYPE_NONF_IPV4_UDP;
使用了 ethtool 设定 user-def 的时候 fd_data->flex_filter 为 true,这时可以看到对 udp 报文的 payload + off 字段的填充逻辑。
I40E_UDPIP_DUMMY_PACKET_LEN 的定义如下:
#define I40E_UDPIP_DUMMY_PACKET_LEN 42
上述代码计算的 payload 的起始位置正是 udp 报文的 data 字段的起始位置。
与此类似,过滤 tcpv4 报文时填充 payload 字段的代码如下:
if (fd_data->flex_filter) {
u8 *payload = raw_packet + I40E_TCPIP_DUMMY_PACKET_LEN;
__be16 pattern = fd_data->flex_word;
u16 off = fd_data->flex_offset;
*((__force __be16 *)(payload + off)) = pattern;
}
I40E_TCPIP_DUMMY_PACKET_LEN 宏的定义如下:
#define I40E_TCPIP_DUMMY_PACKET_LEN 54
可以确定 tcpv4 flex payload 从报文的第 54 字节开始,同理 ipv4 flex payload 从报文的第 34 字节开始,sctp flex payload 从报文的第 46 字节开始。
构造完成一个 raw_packet 后同样需要填充描述符,tx_ring->next_to_use 再次向后拨动一个单位。完成上述过程后,最终将 tx_ring->next_to_use 写入到 tx_ring->tail 以通知网卡处理, tx_ring->tail 是网卡中映射的一个物理地址。
修改代码,打印填充的 raw_packet 报文内容
经过上面对 flex filter 设定过程的研究,我觉得可能是填充的 raw_packet 报文的内容存在问题,故而在驱动中添加了打印报文内容的操作重试。
测试过程记录如下:
[root] # dmesg -c >/dev/null ; ethtool -U enp11s0f0 flow-type udp4 user-def 0x212da action -1; dmesg | grep -A 10 'packet\[42\]'
Added rule with ID 7679
[165607.394687] packet[42] is 0
[165607.394688] packet[43] is 0
[165607.394689] packet[44] is 12
[165607.394691] packet[45] is da
[165607.394692] packet[46] is 0
[165607.394693] packet[47] is 0
[165607.394694] packet[48] is 0
[165607.394696] packet[49] is 0
[165607.394697] packet[50] is 0
[165607.394698] packet[51] is 0
[165607.394700] packet[52] is 0
根据打出的报文内容看,设定是正确的!但是仍旧没有真正生效,看来还是哪里没有整对。
唯一一次成功的 use-def 配置时的 raw_packet 内容
一番折腾后,我想到也许可以把那唯一一次生效的 raw_packet 内容打出来,看看有什么不同,结果也没有找到怀疑点,测试过程还是记录到下面。
[root] # dmesg -c >/dev/null ; ethtool -U enp11s0f0 flow-type udp4 user-def 0x0 action -1; dmesg | grep -A 10 'packet\[42\]'
Added rule with ID 7679
[165763.336866] i40e 0000:0b:00.0 enp11s0f0: Input set change requested for udp4 flows:
[165763.336871] i40e 0000:0b:00.0 enp11s0f0: Current input set: 2000
[165763.336874] i40e 0000:0b:00.0 enp11s0f0: Requested input set: 2000
[165763.336877] i40e 0000:0b:00.0 enp11s0f0: FLEX index 0: Offset -> 0
[165763.337433] packet[0] is 0
[165763.337435] packet[1] is 0
[165763.337436] packet[2] is 0
[165763.337437] packet[3] is 0
[165763.337439] packet[4] is e6
[165763.337440] packet[5] is ff
[165763.337442] packet[6] is ff
[165763.337443] packet[7] is ff
[165763.337444] packet[8] is 0
[165763.337446] packet[9] is 0
[165763.337447] packet[10] is 0
[165763.337448] packet[11] is 0
[165763.337450] packet[12] is 8
[165763.337451] packet[13] is 0
[165763.337452] packet[14] is 45
[165763.337454] packet[15] is 0
[165763.337455] packet[16] is 0
[165763.337456] packet[17] is 1c
[165763.337458] packet[18] is 0
[165763.337459] packet[19] is 0
[165763.337460] packet[20] is 40
[165763.337462] packet[21] is 0
[165763.337463] packet[22] is 40
[165763.337464] packet[23] is 11
总结
x710 flow directory 是个挺高级的功能,但是我们的业务场景中从来没有使用过,没有相应的技术积累,这次搞起来就显得困难重重。
flex filter 功能没有用起来,但是五元组过滤算是上手了,这个功能刚好能够解决我们遇到的问题,不过当场景切换到 dpdk 中时,即便一个简单的过滤 udp 报文的功能也研究了好几天,期间一度想放弃,被搞到想哭,最终搞定了后又激动到想哭。
现在想想在某天 21:00 的时候,我心怀忐忑的准备测试看 dpdk 中的 flow directory 功能是否生效,之前已经失败了好多次了,仔细 check,最终确定成功了后,我真的激动到眼泪快掉下来!
这并不只是技术上的突破,这更是突破自我局限的一次成功案例,我的成就感又回来了!